﻿using NLua;
using OptimizationPlus.Behaviors;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using TaleWorlds.Core;
using TaleWorlds.Engine;
using TaleWorlds.InputSystem;
using TaleWorlds.Library;
using TaleWorlds.MountAndBlade;
using Path = System.IO.Path;

namespace OptimizationPlus
{
    public class OptimizationPlusSubModule : MBSubModuleBase
    {
        private Lua _lua;
        private InputKey _resetKey;
        private readonly List<LuaTable> _tables = new List<LuaTable>();
        private readonly ConcurrentQueue<LuaFunction> _timeouts = new ConcurrentQueue<LuaFunction>();

        private void Debug_OnPrint(string arg1, ulong arg2)
        {
            try
            {
                File.AppendAllText(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "debug.log"), string.Format("[{0}] {1}{2}", DateTime.Now, arg1, Environment.NewLine));
            }
            catch
            {
            }
        }

        protected override void OnSubModuleLoad()
        {
            try
            {
                File.WriteAllText(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "debug.log"), "");
                Debug.OnPrint -= Debug_OnPrint;
                Debug.OnPrint += Debug_OnPrint;

                Debug.Print("[Lua] load...");

                var root = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                var keyPath = Path.Combine(root, "hotkey");

                _resetKey = (File.Exists(keyPath) && Enum.TryParse<InputKey>(File.ReadAllText(keyPath), true, out var key)) ? key : InputKey.Insert;

                Debug.Print("[Lua] reset hotkey: " + _resetKey.ToString());

                _lua = new Lua(true);
                _lua.LoadCLRPackage();
                _lua.State.Encoding = Encoding.UTF8;
                _lua["SubModule"] = this;
                _tables.Clear();

                var lua = Path.Combine(root, "lua");
                if (Directory.Exists(lua))
                {
                    Debug.Print("[Lua] run scripts...");

                    var main = Path.Combine(lua, "main.lua");

                    Debug.Print("[Lua] run " + main);

                    if (_lua.DoFile(main)[0] is LuaTable t)
                    {
                        _tables.Add(t);
                    }

                    Debug.Print("[Lua] run done");

                    var banlistPath = Path.Combine(lua, "banlist.txt");
                    var banlist = File.Exists(banlistPath) ? File.ReadAllLines(banlistPath) : new string[0];
                    var scripts = Directory.EnumerateFiles(lua, "*.lua", SearchOption.AllDirectories);

                    foreach (string script in scripts)
                    {
                        try
                        {
                            if (banlist.Contains(Path.GetFileName(script)) || script.Equals(main, StringComparison.OrdinalIgnoreCase))
                            {
                                continue;
                            }

                            Debug.Print("[Lua] run " + script);

                            if (_lua.DoFile(script)[0] is LuaTable table)
                            {
                                _tables.Add(table);
                            }

                            Debug.Print("[Lua] run done");
                        }
                        catch (Exception ex)
                        {
                            Debug.Print("[Lua] run error: " + ex.ToString(), 2, Debug.DebugColor.Red);
                        }
                    }

                    InvokeFunction("onSubModuleLoad");
                    InvokeFunction("load");
                }

                Debug.Print("[Lua] load done");
            }
            catch (Exception ex)
            {
                Debug.Print(ex.ToString());
            }
        }

        protected override void OnSubModuleUnloaded()
        {
            InvokeFunction("onSubModuleUnloaded");
            InvokeFunction("unload");
            _tables.Clear();
            _lua.Close();
        }

        protected override void OnBeforeInitialModuleScreenSetAsRoot()
        {
            InvokeFunction("onBeforeInitialModuleScreenSetAsRoot");
        }

        public override void OnConfigChanged()
        {
            InvokeFunction("onConfigChanged");
        }

        protected override void OnGameStart(Game game, IGameStarter gameStarterObject)
        {
            InvokeFunction("onGameStart", game, gameStarterObject);
        }

        protected override void OnApplicationTick(float dt)
        {
            if (Input.IsKeyPressed(_resetKey))
            {
                Debug.Print("[Lua] reset...");

                _lua.Close();
                OnSubModuleLoad();

                Debug.Print("[Lua] reset done");
            }
            else
            {
                InvokeFunction("onApplicationTick", dt);
                InvokeFunction("tick", dt);

                try
                {
                    while (_timeouts.TryDequeue(out var timeout))
                    {
                        timeout.Call(dt);
                    }
                }
                catch (Exception ex)
                {
                    var msg = "[Lua] error: " + ex.ToString();
                    Debug.Print(msg, 2, Debug.DebugColor.Red);
                    InformationManager.DisplayMessage(new InformationMessage(msg, Colors.Red));
                }
            }
        }

        protected override void AfterAsyncTickTick(float dt)
        {
            InvokeFunction("afterAsyncTickTick", dt);
        }

        protected override void InitializeGameStarter(Game game, IGameStarter starterObject)
        {
            InvokeFunction("initializeGameStarter", game, starterObject);
        }

        public override void OnGameLoaded(Game game, object initializerObject)
        {
            InvokeFunction("onGameLoaded", game, initializerObject);
        }

        public override void OnNewGameCreated(Game game, object initializerObject)
        {
            InvokeFunction("onNewGameCreated", game, initializerObject);
        }

        public override void BeginGameStart(Game game)
        {
            InvokeFunction("beginGameStart", game);
        }

        public override void OnCampaignStart(Game game, object starterObject)
        {
            InvokeFunction("onCampaignStart", game, starterObject);
        }

        public override void RegisterSubModuleObjects(bool isSavedCampaign)
        {
            InvokeFunction("registerSubModuleObjects", isSavedCampaign);
        }

        public override void AfterRegisterSubModuleObjects(bool isSavedCampaign)
        {
            InvokeFunction("afterRegisterSubModuleObjects", isSavedCampaign);
        }

        public override void OnMultiplayerGameStart(Game game, object starterObject)
        {
            InvokeFunction("onMultiplayerGameStart", game, starterObject);
        }

        public override void OnGameInitializationFinished(Game game)
        {
            InvokeFunction("onGameInitializationFinished", game);
        }
        public override void OnAfterGameInitializationFinished(Game game, object starterObject)
        {
            InvokeFunction("onAfterGameInitializationFinished", game, starterObject);
        }

        public override bool DoLoading(Game game)
        {
            InvokeFunction("doLoading", game);
            return true;
        }

        public override void OnGameEnd(Game game)
        {
            InvokeFunction("onGameEnd", game);
        }

        public override void OnMissionBehaviorInitialize(Mission mission)
        {
            InvokeFunction("onMissionBehaviorInitialize", mission);
        }

        public override void OnBeforeMissionBehaviorInitialize(Mission mission)
        {
            InvokeFunction("onBeforeMissionBehaviorInitialize", mission);
        }

        public override void OnInitialState()
        {
            InvokeFunction("onInitialState");
        }

        protected override void OnNetworkTick(float dt)
        {
            InvokeFunction("onNetworkTick", dt);
        }

        private void InvokeFunction(string methodName, params object[] args)
        {
            foreach (var t in _tables.ToArray())
            {
                try
                {
                    if (t[methodName] is LuaFunction func)
                    {
                        func.Call(args);
                    }
                }
                catch (Exception ex)
                {
                    var msg = "[Lua] error: " + ex.ToString();
                    Debug.Print(msg, 2, Debug.DebugColor.Red);
                    InformationManager.DisplayMessage(new InformationMessage(msg, Colors.Red));
                    _tables.Remove(t);
                }
            }
        }

        public void SetTimeout(int millisecondsDelay, LuaFunction func)
        {
            Task.Run(async () =>
            {
                await Task.Delay(millisecondsDelay);
                _timeouts.Enqueue(func);
            });
        }

        public object GetPropertyValue(Type type, string propertyName, object @object, bool isPublic = true)
        {
            var flags = BindingFlags.NonPublic;
            if (isPublic)
            {
                flags = BindingFlags.Public;
            }
            if (@object == null)
            {
                flags |= BindingFlags.Static;
            }
            else
            {
                flags |= BindingFlags.Instance;
            }

            var property = type.GetProperty(propertyName, flags);
            return property?.GetValue(@object, null);
        }

        public void SetPropertyValue(Type type, string propertyName, object @object, object value, bool isPublic = true)
        {
            BindingFlags flags = BindingFlags.NonPublic;
            if (isPublic)
            {
                flags = BindingFlags.Public;
            }
            if (@object == null)
            {
                flags |= BindingFlags.Static;
            }
            else
            {
                flags |= BindingFlags.Instance;
            }

            var property = type.GetProperty(propertyName, flags);
            if (property != null)
            {
                if (value != null && value.GetType() != property.PropertyType)
                {
                    value = Convert.ChangeType(value, property.PropertyType);
                }
                property.SetValue(@object, value, null);
            }
            else
            {
                var msg = $"[Lua] error: {type.FullName}.{propertyName} not found";
                Debug.Print(msg, 2, Debug.DebugColor.Red);
            }
        }

        //public LuaCampaignBehavior CreateCampaignBehavior(LuaTable table)
        //{
        //    return new LuaCampaignBehavior(table);
        //}

        public LuaMissionBehavior CreateMissionBehavior(LuaTable table)
        {
            return new LuaMissionBehavior(table);
        }

        public void CreateParticleSystemAttachedToBone(MBAgentVisuals agentVisuals, string particleName, HumanBone bone)
        {
            var identity = MatrixFrame.Identity;
            agentVisuals.CreateParticleSystemAttachedToBone(particleName, agentVisuals.GetRealBoneIndex(bone), ref identity);
        }

        public MBList<Agent> CreateAgentList() => new MBList<Agent>();

        public List<CompassItemUpdateParams> CreateCompassTargets() => new List<CompassItemUpdateParams>();

        public Blow CreateMissileBlow(Agent attackerAgent, AttackCollisionData collisionData, MissionWeapon attackerWeapon, Vec3 missilePosition, Vec3 missileStartingPosition)
        {
            Mission mission = Mission.Current;
            if (mission == null) return new Blow(-1);

            var type = mission.GetType();
            var methodInfo = type.GetMethod("CreateMissileBlow", BindingFlags.Instance | BindingFlags.NonPublic);
            return methodInfo.Invoke(mission, new object[] { attackerAgent, collisionData, attackerWeapon, missilePosition, missileStartingPosition }) is Blow b ? b : new Blow(-1);
        }

        public Mission.Missile GetMissile(int index)
        {
            Mission mission = Mission.Current;
            if (mission == null) return null;

            var type = mission.GetType();
            var fieldInfo = type.GetField("_missiles", BindingFlags.Instance | BindingFlags.NonPublic);
            if (fieldInfo?.GetValue(mission) is Dictionary<int, Mission.Missile> missiles && missiles.TryGetValue(index, out var missile))
                return missile;
            return null;
        }

        public MBList<Mission.Missile> GetMissiles() => Mission.Current?.Missiles.ToMBList() ?? new MBList<Mission.Missile>();

        public MissionObject GetFirstMissionObject(GameEntity entity) => entity?.GetFirstScriptOfType<MissionObject>();
    }
}
